Elementarne struktury danych: drzewo binarne & kolejka priorytetowa

Oglnie rzecz biorc drzewo binarne to takie w ktrym kady element (poza korzeniem) ma ojca, oraz dwch, jednego, bd zero synw (w takim przypadku element nazywany jest liciem). Pene drzewo binarne to takie, w ktrym tylko najniszy poziom moe by niepeny, lecz owa niepeno musi by, e tak powiem, uporzdkowana. To znaczy jakby rozrysowa sobie takie drzewko, to jeeli wystpuje spjny cig elementw od lewej strony na najniszym poziomie i aden inny element na tym poziomie si nie znajduje to drzewo binarne jest pene. 

Pene drzewo binarne jest wyjtkowo mi struktur, bowiem mona umieci j w tablicy bez adnych wskanikw do synw i ojcw, poniewa mona sobie offsety do nich policzy. I tak zakadajc e korze jest elementem zerowym tablicy (m[0]) to majc element zajmujcy pozycj x w tablicy (m[x]) moemy znale jego ojca (m[(x-1)/2]) jego lewego syna (m[x*2+1]) i prawego (m[x*2+2]).

Z takim penym drzewem binarnym moemy zrobi wiele ciekawych rzeczy. Moemy zrobi sobie z tego na przykad kolejk priorytetow, albo napisa oparte na nim sortowanie (dziaajce niby w O(nlogn), ale i tak quicksort okazuje si zazwyczaj szybszy). Kolejka priorytetowa jest do przydatn struktur, poniewa pozwala znale najwikszy element (bd najmniejszy, zaley jak sobie j napiszemy, niestety nie jest moliwe szukanie min & max w jednej kolejce) w czasie O(1), mona te znale i usun najwikszy element w czasie O(logn) i w takim samym czasie doda nowy element. Czas logn dla 65536 wynosi po prostu 16, wic sami widzicie, e jest to szybka i efektowna struktura. A oto przykadowa implementacja (akurat do wyszukiwania minimum):

class heap {
 public:
  long n, m[10000];

  void insert (long x)
  {
   m[n] = x;
   if (n>0) push_up (n);
   n++;
  }
  long delmin (void)
  {
   long tmp = m[0];
   m[0] = m[--n];
   push_down (0);
   return tmp;
  }
  long min (void)
  {
   return m[0];
  }

  void push_up (long x)
  {
   while (m[x]<m[(x-1)/2])
   {
    long tmp = m[x];
    m[x] = m[(x-1)/2];
    m[(x-1)/2] = tmp;
    if (x>0) x = (x-1)/2;
   }
  }
  void push_down (long x)
  {
   while (1)
   {
    long l=(x*2)+1;
    long r=(x*2)+2;
    long min = x;
    if ((l<n)&&(m[l]<m[min]))
     min = l;
    if ((r<n)&&(m[r]<m[min]))
     min = r;
    if (min!=x)
    {
     long tmp = m[min];
     m[min] = m[x];
     m[x] = tmp;
     x = min;
    } else break;
   }
  }
} h;

Jak wida powysza implementacja jest iteracyjna, nie rekurencyjna. Mimo e wersja rekurencyjna jest duo, duo prostsza do napisania, a i raczej trudno przepeni ni stos (w kocu maksymalne zagbienie si procedury dla n elementw wynosi lg(n), to mimo to czsto lepiej stosowa wanie wersj iteracyjn. Powody? Jest szybsza. Mimo i rnice midzy jedn a drug wersj s minimalne, to przy wykonywaniu kilku milionw wywoa zdejmowania/dodawania do stosu, rnica jest zauwaalna (tzn. wersja iteracyjna dziaa o par/parnacie setnych sekund szybciej, co moe mie znaczenie). Najwaniejsze s tu dwie procedury - push_down i push_up, ktre su do manipulowania elementami tak, eby zachowa waciwo stosu (tzn. w tym przypadku aden z synw nie moe by mniejszy od ojca). Czsto koduje si je po prostu jako integralna cz procedur insert i delmin, jednak napisanie ich osobno pozwala na przywracanie waciwoci stosu w momencie zmiany klucza dla dowolnego elementu stosu. Dziaanie procedury push_down jest nastpujce: sprawdzamy ktry z trzech elementw - ojca i jego dwch synw - jest najmniejszy. Jeeli jest to ojciec, to waciwoci stosu s zachowane wic przerywamy dziaanie. W Przypadku, w ktrym jest to ktry z synw zamieniamy go z ojcem i ca procedur powtarzamy tym razem dla niego. Dziaanie push_up polega na sprawdzaniu, czy dany element jest mniejszy od ojca. Jeli tak, to zamieniamy go z nim i powtarzamy czynnoci dla ojca. Easy?:) 

nostre/marsmellow

